<?php

/**
 * @package Bike
 * @subpackage Storage
 */

/**
 * Nested sets storage class
 * @author Chrome
 * @package Bike
 * @subpackage Storage
 */
class Bike_Storage_Tree_NestedSets extends Bike_Storage_List {

	/**
	 * Default fields
	 * @var array
	 */
	protected $fields = array('id' => array('type' => 'integer', 'title' => 'Id of record'),
							  'level' => array('type' => 'integer', 'title' => 'level'),
							  'left_key' => array('type' => 'integer', 'title' => 'left key'),
							  'right_key' => array('type' => 'integer', 'title' => 'right key'),
							  'label' => array('type' => 'string', 'title' => 'label'),
							  'uri' => array('type' => 'string', 'title' => 'uri'));

	/**
	 * Table prefix
	 * @var string
	 */
	protected $prefix = 'nst_';

	/**
	 * Get node by uri
	 * @param string $uri
	 * @return array
	 * @throws Bike_Storage_Exception
	 */
	public function getNodeByUri($uri = ''){
		if(empty($uri)){
			throw new Bike_Storage_Exception('Incorrect arguments passed', Bike_Storage_Exception::INCORRECT_ARGUMENTS_PASSED);
		}
		$filters = array(array('field' => 'uri', 'operation' => '=', 'value' => $uri));
		$resultData = array();
		$data = $this->getRecordsByConditions($filters, 0, 1);
		foreach($data['items'] as $dataPart){
			$resultData = $this->prepareData($dataPart);
		}
		return $resultData;
	}

	/**
	 * Get node by $id
	 * @param int $id
	 * @return array
	 * @throws Bike_Storage_Exception
	 */
	public function getNodeById($id = 0){
		$id = intval($id);
		if(empty($id)){
			throw new Bike_Storage_Exception('Incorrect arguments passed', Bike_Storage_Exception::INCORRECT_ARGUMENTS_PASSED);
		}
		$filters = array(array('field' => 'id', 'operation' => '=', 'value' => $id));
		$resultData = array();
		$data = $this->getRecordsByConditions($filters, 0, 1);
		foreach($data['items'] as $dataPart){
			$resultData = $this->prepareData($dataPart);
		}
		return $resultData;
	}

	
	/**
	 * Get children for given node
	 * @param array $node
	 * @param boolean $immediate
	 * @param array $sortings
	 * @throws Bike_Storage_Exception
	 * @return array
	 */
	public function getChildren(array $node, $immediate = false, array $sortings = array()){
		if(
			empty($node['left_key']) || 0 == intval($node['left_key']) ||
			empty($node['right_key']) || 0 == intval($node['right_key']) ||
			empty($node['level']) || 0 == intval($node['level'])
			){
			throw new Bike_Storage_Exception('Incorrect arguments passed', Bike_Storage_Exception::INCORRECT_ARGUMENTS_PASSED);
		}
		$node['left_key'] = intval($node['left_key']);
		$node['right_key'] = intval($node['right_key']);
		$node['level'] = intval($node['level']);
		$filters = array(
			array('field' => 'left_key', 'operation' => '>', 'value' => $node['left_key']),
			array('field' => 'right_key', 'operation' => '<', 'value' => $node['right_key'])
		);
		if($immediate){
			$filters [] = array('field' => 'level', 'operation' => '=', 'value' => $node['level'] + 1);
		}
		$data = $this->getRecordsByConditions($filters, 0, -1, $sortings);
		$resultData = array();
		foreach($data['items'] as $dataPart){
			$resultData[] = $this->prepareData($dataPart);
		}
		return $resultData;
	}

	/**
	 * Insert node as child of given node
	 * @param int $parentNodeId
	 * @param array $data
	 * @throws Bike_Storage_Exception
	 * @return boolean
	 */
	public function insertNode($parentNodeId = 0, array $data){
		$parentNodeData = $this->getNodeById($parentNodeId);
		if(empty($parentNodeData)){
			return false;
		}
		if(empty($data['label'])){
			throw new Bike_Storage_Exception('Incorrect arguments passed', Bike_Storage_Exception::INCORRECT_ARGUMENTS_PASSED);
		}
		$data = $this->prepareData($data);
		$data['left_key'] = $parentNodeData['right_key'];
		$data['right_key'] = $data['left_key'] + 1;
		$data['uri'] = $parentNodeData['uri'] . $data['label'] . '/';
		$data['level'] = $parentNodeData['level'] + 1;
		$existingNodeData = $this->getNodeByUri($data['uri']);
		if(!empty($existingNodeData)){
			return false;
		}
		$filters = array(
			array('field' => 'right_key', 'operation' => '>=', 'value' => $parentNodeData['right_key'])
		);
		$recordsForUpdate = $this->getRecordsByConditions($filters, 0,-1);
		foreach($recordsForUpdate['items'] as $recordForUpdate){
			$recordForUpdate['right_key'] +=2;
			$recordForUpdate = $this->prepareData($recordForUpdate);
			$this->updateRecord($recordForUpdate['id'], $recordForUpdate);
		}
		$filters = array(
			array('field' => 'left_key', 'operation' => '>', 'value' => $parentNodeData['right_key'])
		);
		$recordsForUpdate = $this->getRecordsByConditions($filters, 0,-1);
		foreach($recordsForUpdate['items'] as $recordForUpdate){
			$recordForUpdate['left_key'] +=2;
			$recordForUpdate = $this->prepareData($recordForUpdate);
			$this->updateRecord($recordForUpdate['id'], $recordForUpdate);
		}
		return $result = $this->insertRecord($data);
	}

	/**
	 * Update node with id = $nodeId using given data and its children if needed
	 * @param int $nodeId
	 * @param array $data
	 * @throws Bike_Storage_Exception
	 * @return boolean
	 */
	public function updateNode($nodeId, array $data){
		$nodeId = intval($nodeId);
		$oldData = $this->getNodeById($nodeId);
		if(empty($oldData)){
			return false;
		}
		$data = $this->prepareData($data);
		$result =  $this->updateRecord($nodeId, $data);
		if($oldData['label'] != $data['label']){
			$data['uri'] = str_replace('/' . $oldData['label'] . '/', '/' . $data['label'] . '/', $oldData['uri']);
			$this->updateRecord($nodeId, $data);
			$children = $this->getChildren($oldData, false);
			foreach($children as &$child){
				$child['uri'] = str_replace('/' . $oldData['label'] . '/', '/' . $data['label'] . '/', $child['uri']);
				$child = $this->prepareData($child);
				$this->updateRecord($child['id'], $child);
			}
		}
		return $result;
	}

	/**
	 * Drop node with given $nodeId and its children if any
	 * @param int $nodeId
	 * @return boolean
	 */
	public function dropNode($nodeId = 0){
		$nodeId = intval($nodeId);
		$nodeData = $this->getNodeById($nodeId);
		if(empty($nodeData)){
			return false;
		}
		$children = $this->getChildren($nodeData);
		$childrenCount = 0;
		foreach($children as &$child){
			$childrenCount++;
			$this->deleteRecord($child['id']);
		}
		$delta = $childrenCount * 2 + 2;
		$result = $this->deleteRecord($nodeId);
		$filters = array(
			array('field' => 'right_key', 'operation' => '>=', 'value' => $nodeData['right_key'])
		);
		$recordsForUpdate = $this->getRecordsByConditions($filters, 0, -1);
		foreach($recordsForUpdate['items'] as $recordForUpdate){
			$recordForUpdate['right_key'] -= $delta;
			if($nodeData['left_key'] < $recordForUpdate['left_key']){
				$recordForUpdate['left_key'] -= $delta;
			}
			$recordForUpdate = $this->prepareData($recordForUpdate);
			$this->updateRecord($recordForUpdate['id'], $recordForUpdate);
		}
		return $result;
	}

	/**
	 * Return root node data
	 * @return array
	 */
	public function getRootNode(){
		$filters = array(
		  array('field' => 'level', 'operation' => '=', 'value' => 1)
		);
		$nodeData = $this->getRecordsByConditions($filters, 0, 1);
		$resultData = array();
		foreach($nodeData['items'] as $dataPart){
			$resultData = $this->prepareData($dataPart);
		}
		return $resultData;
	}
}
